package chapter

import scala.annotation.tailrec

object Chapter5 extends App {

  /*  val list = List(('a', 8), ('b', 1), ('a', 7)).groupBy(_._1).map {
    case (which, counts) =>
      (which, counts.foldLeft(0)(_ + _._2))
  }.toSeq.sortBy(_._2).reverse

  println(list)*/

  import scala.io.Source
  import scala.util.Try

  val lines = Try { Source.fromFile("src/stu/ebook/functional programming").getLines }.get

  println(lines.mkString)

  sealed trait List[+A]
  case object Nil extends List[Nothing]
  case class Cons[+A](head: A, tail: List[A]) extends List[A]
  object List {
    def sum(ints: List[Int]): Int = ints match {
      case Nil => 0
      case Cons(x, xs) => x + sum(xs)
    }
    def product(ds: List[Double]): Double = ds match {
      case Nil => 1.0
      case Cons(0.0, _) => 0.0
      case Cons(x, xs) => x * product(xs)
    }
    def tail[A](list: List[A]) = list match {
      case Nil => Nil
      case Cons(_, tails) => tails
    }
    @tailrec
    def drop[A](list: List[A], n: Int): List[A] = n match {
      case 0 => list
      case x => drop(tail(list), n - 1)
    }
    @tailrec
    def dropWhile[A](list: List[A])(f: A => Boolean): List[A] = list match {
      case Nil => Nil
      case Cons(head, tail) if (f(head)) => dropWhile(tail)(f)
      case rest => rest
    }
    def append[A](a1: List[A], a2: List[A]): List[A] =
      a1 match {
        case Nil => a2
        case Cons(h, t) => Cons(h, append(t, a2))
      }
    //List(1,2,3,4) => List(1,2,3)
    def init[A](list: List[A]): List[A] = reverse(drop(reverse(list), 1))

    def foldLeft[A, B](l: List[A], z: B)(f: (B, A) => B): B = {
      @tailrec
      def loop(acc: B, rest: List[A]): B = rest match {
        case Nil => acc
        case h Cons t => loop(f(acc, h), t)
      }
      loop(z, l)
    }

    def foldRight[A, B](l: List[A], z: B)(f: (A, B) => B): B = {
      def loop(list: List[A], z: B)(f: (A, B) => B): B = list match {
        case Nil => z
        case h Cons t => f(h, loop(t, z)(f))
      }
      loop(reverse(l), z)(f)
    }

    def length[A](as: List[A]): Int = foldLeft(as, 0)((b, _) => b + 1)

    def sum[A](l: List[A])(implicit numeric: Numeric[A]): A = {
      foldLeft(l, numeric.zero)(numeric.plus)
    }

    def product[A: Numeric](l: List[A])(implicit numeric: Numeric[A]): A = {
      foldLeft(l, numeric.one)(numeric.times)
    }

    def reverse[A](l: List[A]): List[A] =
      foldLeft(l, List[A]()) { (t, h) => Cons(h, t) }

    def map[A, B](list: List[A])(f: A => B): List[B] = {
      def loop(acc: List[B], rest: List[A]): List[B] = rest match {
        case Nil => Nil
        case head Cons tail => loop(Cons(f(head), acc), tail)
      }
      loop(List[B](), list)
    }

    def mapViaFoldLeft[A, B](list: List[A])(f: A => B): List[B] =
      foldLeft(list, List[B]()) { (acc, rest) => Cons(f(rest), acc) }

    def filter[A](list: List[A])(p: A => Boolean): List[A] = {
      @tailrec
      def loop(acc: List[A], rest: List[A]): List[A] = rest match {
        case Nil => reverse(acc)
        case head Cons tail if p(head) => loop(Cons(head, acc), tail)
        case head Cons tail => loop(acc, tail)
      }
      loop(List[A](), list)
    }

    def flatMap[A, B](as: List[A])(f: A => List[B]): List[B] = {
      @tailrec
      def loop(acc: List[B], rest: List[A]): List[B] = {
        rest match {
          case Nil => acc
          case head Cons tail => loop(append(acc, f(head)), tail)
        }
      }
      loop(List[B](), as)
    }

    def filterViaFlatMap[A](as: List[A])(f: A => Boolean): List[A] = {
      flatMap(as) { a => if (f(a)) List(a) else Nil }
    }

    def apply[A](as: A*): List[A] =
      if (as.isEmpty) Nil
      else Cons(as.head, apply(as.tail: _*))

  }

  println(List(1, 2, 3) match { case Cons(_, t) => t; case _ => })

  val x = List(1, 2, 3, 4, 5) match {
    case Cons(x, Cons(2, Cons(4, _))) => x
    case Nil => 42
    //    case Cons(x, Cons(y, Cons(3, Cons(4, _)))) => x + y
    case Cons(h, t) => h + List.sum(t)
    case _ => 101
  }

  println(List.append(List(1, 2, 3), List(0, 4, 5)))
  println(List.length(List(1, 2, 3, 4)))

}

